/*
 * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.xuexiang.xdlink.component.channel.impl.bt;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.text.TextUtils;

import com.xuexiang.xdlink.component.channel.entity.ConnectStatusEnum;
import com.xuexiang.xdlink.component.channel.impl.AbstractChannel;
import com.xuexiang.xdlink.exception.ChannelException;
import com.xuexiang.xdlink.logs.DCFLog;

import java.util.List;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 低功耗蓝牙通信通道
 *
 * @author xuexiang
 * @since 2018/9/7 下午9:53
 */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class BLEChannel extends AbstractChannel {

    private Context mContext;
    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothGatt mBluetoothGatt;

    /**
     * 蓝牙地址
     */
    private String mMacAddress;
    /**
     * 写特征字
     */
    protected CharacteristicInfo mWriteCharacteristicInfo;
    /**
     * 读特征字
     */
    protected CharacteristicInfo mReadCharacteristicInfo;

    /**
     * 写队列【解决大数据需要分包，但是低功耗蓝牙是异步写入的问题】
     */
    private Queue<byte[]> mWriteQueue = new ConcurrentLinkedQueue<>();
    /**
     * 当前是否在写数据
     */
    private volatile boolean mIsWriting = false;

    /**
     * 构造方法
     *
     * @param builder
     */
    public BLEChannel(Builder builder) {
        mContext = builder.context;
        mMacAddress = builder.macAddress;
        mWriteCharacteristicInfo = builder.writeCharacteristicInfo;
        mReadCharacteristicInfo = builder.readCharacteristicInfo;
        initBluetoothAdapter(mContext);
    }

    /**
     * 初始化蓝牙适配器
     *
     * @return {@code true}: 初始化成功 <br> {@code false}：初始化失败
     */
    private boolean initBluetoothAdapter(Context context) {
        if (mBluetoothManager == null) {
            mBluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
            if (mBluetoothManager == null) {
                return false;
            }
        }
        mBluetoothAdapter = mBluetoothManager.getAdapter();
        return mBluetoothAdapter != null;
    }

    //======================连接=========================//

    @Override
    public void openChannel() {
        if (!connectBLE()) {
            updateConnectStatus(ConnectStatusEnum.CONNECT_FAILED);
        }
    }

    /**
     * 连接BLE设备
     *
     * @return 是否开始连接
     */
    private boolean connectBLE() {
        DCFLog.d("Discover BLE device and start connecting...");
        if (mBluetoothAdapter == null || !BluetoothAdapter.checkBluetoothAddress(mMacAddress)) {
            return false;
        }

        if (mBluetoothGatt != null) {
            return mBluetoothGatt.connect();
        }

        BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(mMacAddress);
        if (device != null) {
            mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback);
            return true;
        } else {
            return false;
        }
    }

    /**
     * BLE服务回调【包括设备连接状态回调、读写回调等方法】
     */
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothProfile.STATE_CONNECTED) { //连接成功
                    mBluetoothGatt.discoverServices(); //开始搜索服务
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {  //断开成功
                    updateConnectStatus(ConnectStatusEnum.DISCONNECT_SUCCESS);
                    close();
                }
            } else if (status == 133 || status == 62) {
                close();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                connectBLE();
            } else if (status == BluetoothGatt.GATT_FAILURE) { //错误的操作

            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (mBluetoothGatt != null) {
                    onGetCharacteristics(mBluetoothGatt);
                    updateConnectStatus(ConnectStatusEnum.CONNECT_SUCCESS);
                } else {
                    updateConnectStatus(ConnectStatusEnum.CONNECT_FAILED);
                }
            } else {
                updateConnectStatus(ConnectStatusEnum.CONNECT_FAILED);
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            if (mReadCharacteristicInfo != null && mReadCharacteristicInfo.isCharacteristicEquals(characteristic)) {
                byte[] receivedData = characteristic.getValue();
                DCFLog.d("BLE receive data, data length：" + receivedData.length);
                onDataReceived(BLEChannel.this, receivedData, receivedData.length);
            }
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (mWriteCharacteristicInfo != null && mWriteCharacteristicInfo.isCharacteristicEquals(characteristic)) {
                    byte[] writeData = characteristic.getValue();
                    DCFLog.d("BLE data sent successfully，data length：" + writeData.length);
                    nextWrite();
                }
            } else if (status == BluetoothGatt.GATT_WRITE_NOT_PERMITTED) {
                DCFLog.e("BLE send data failed, not permitted! status:" + status);
            } else {
                DCFLog.e("BLE send data failed!status:" + status);
            }
        }

        @Override
        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
        }
    };

    /**
     * 搜索到低功耗蓝牙设备后，获取特征字段
     */
    private void onGetCharacteristics(BluetoothGatt bluetoothGatt) {
        mWriteCharacteristicInfo.setGattCharacteristic(bluetoothGatt);
        mReadCharacteristicInfo.setGattCharacteristic(bluetoothGatt);

        setCharacteristicNotification(mReadCharacteristicInfo.getGattCharacteristic(), true);
    }

    /**
     * @return 获取支持BLE的设备列表
     */
    public List<BluetoothGattService> getSupportedGattServices() {
        if (mBluetoothGatt == null) {
            return null;
        }
        return mBluetoothGatt.getServices();
    }

    //======================关闭=========================//

    @Override
    public void closeChannel() {
        if (mBluetoothAdapter != null && mBluetoothGatt != null) {
            mBluetoothGatt.disconnect();
        }
    }

    @Override
    public void close() {
        updateOpenedStatus(false);
        if (mBluetoothGatt != null) {
            mBluetoothGatt.close();
            mBluetoothGatt = null;
        }
        if (mWriteQueue != null) {
            mWriteQueue.clear();
        }
    }

    //==================================写数据=======================================//
    /**
     * 同步锁，防止同步写入导致数据错乱
     */
    private final Lock mLock = new ReentrantLock();

    @Override
    public void write(byte[] message) throws ChannelException {
        mLock.lock();
        try {
            if (mWriteQueue.isEmpty() && !mIsWriting) {
                writeCharacteristic(message);
            } else {
                mWriteQueue.add(message);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            mLock.unlock();
        }
    }

    /**
     * 如果队列中有数据，就继续写
     */
    private void nextWrite() {
        mLock.lock();
        try {
            if (!mWriteQueue.isEmpty()) {
                writeCharacteristic(mWriteQueue.poll());
            } else {
                mIsWriting = false;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            mLock.unlock();
        }
    }

    private void writeCharacteristic(byte[] data) {
        writeCharacteristic(mWriteCharacteristicInfo.getGattCharacteristic(), data);
    }

    private void writeCharacteristic(BluetoothGattCharacteristic writeCharacteristic, byte[] data) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            return;
        }
        mIsWriting = true;
        writeCharacteristic.setValue(data);
        mBluetoothGatt.writeCharacteristic(writeCharacteristic);
    }


    //==================================设置数据接收监听通知=======================================//

    private static final UUID CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");

    /**
     * Enables or disables notification on a give characteristic.
     *
     * @param readCharacteristic Characteristic to act on.
     * @param enabled            If true, enable notification. False otherwise.
     */
    public void setCharacteristicNotification(BluetoothGattCharacteristic readCharacteristic, boolean enabled) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            return;
        }
        if (0 != (readCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY)) { // 查看是否带有可通知属性notify
            mBluetoothGatt.setCharacteristicNotification(readCharacteristic, enabled);
            BluetoothGattDescriptor descriptor = readCharacteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
            if (descriptor != null) {
                descriptor.setValue(enabled ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
                internalWriteDescriptorWorkaround(descriptor);
            }
        } else if (0 != (readCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE)) { // 查看是否带有indecation属性
            mBluetoothGatt.setCharacteristicNotification(readCharacteristic, enabled);
            BluetoothGattDescriptor descriptor = readCharacteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
            if (descriptor != null) {
                descriptor.setValue(enabled ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
                internalWriteDescriptorWorkaround(descriptor);
            }
        }
    }

    /**
     * There was a bug in Android up to 6.0 where the descriptor was written using parent
     * characteristic's write type, instead of always Write With Response, as the spec says.
     * <p>
     * See: <a href="https://android.googlesource.com/platform/frameworks/base/+/942aebc95924ab1e7ea1e92aaf4e7fc45f695a6c%5E%21/#F0">
     * https://android.googlesource.com/platform/frameworks/base/+/942aebc95924ab1e7ea1e92aaf4e7fc45f695a6c%5E%21/#F0</a>
     * </p>
     *
     * @param descriptor the descriptor to be written
     * @return the result of {@link BluetoothGatt#writeDescriptor(BluetoothGattDescriptor)}
     */
    private boolean internalWriteDescriptorWorkaround(final BluetoothGattDescriptor descriptor) {
        final BluetoothGatt gatt = mBluetoothGatt;
        if (gatt == null || descriptor == null)
            return false;

        final BluetoothGattCharacteristic parentCharacteristic = descriptor.getCharacteristic();
        final int originalWriteType = parentCharacteristic.getWriteType();
        parentCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
        final boolean result = gatt.writeDescriptor(descriptor);
        parentCharacteristic.setWriteType(originalWriteType);
        return result;
    }

    //=============================构建者==============================//
    
    /**
     * 构建者
     */
    public static class Builder {
        Context context;
        /**
         * 蓝牙地址
         */
        String macAddress;
        /**
         * 写特征字
         */
        CharacteristicInfo writeCharacteristicInfo;
        /**
         * 读特征字
         */
        CharacteristicInfo readCharacteristicInfo;

        public Builder(Context context) {
            this.context = context.getApplicationContext();
        }

        public Builder setMacAddress(String macAddress) {
            this.macAddress = macAddress;
            return this;
        }

        public Builder setWriteCharacteristicInfo(CharacteristicInfo writeCharacteristicInfo) {
            this.writeCharacteristicInfo = writeCharacteristicInfo;
            return this;
        }

        public Builder setReadCharacteristicInfo(CharacteristicInfo readCharacteristicInfo) {
            this.readCharacteristicInfo = readCharacteristicInfo;
            return this;
        }

        public BLEChannel build() {
            testArguments();
            return new BLEChannel(this);
        }

        private void testArguments() {
            if (TextUtils.isEmpty(this.macAddress)) {
                throw new IllegalArgumentException("BLEChannel macAddress must be set！");
            }
            if (writeCharacteristicInfo == null) {
                throw new IllegalArgumentException("BLEChannel writeCharacteristicInfo must be set！");
            }
            if (readCharacteristicInfo == null) {
                throw new IllegalArgumentException("BLEChannel readCharacteristicInfo must be set！");
            }
        }
    }
}
